iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Software Development

異步程式設計修煉:Kotlin Coroutines 完全指南系列 第 20 篇

Day 19:從回傳一個值到回傳一串值,Flow 是什麼

  • 分享至 

  • xImage
  •  

Day 18:小結:協程的執行環境與例外治理工具箱總覽 結尾把系列至今出現過的所有任務攤開來看,下載一張圖片,或者聚合多個來源的結果,本質上都是執行一次、拿到一個結果的形式。

文章停在一個問題上:執行一次拿到一個結果只是眾多任務形式裡的一種,真正還沒被回答的是,一個任務如果需要隨著時間持續、依序回報多個值,前面累積的所有工具,沒有一個是為了這種情境設計的。今天要正式回答這個問題。

一個結果不夠用的時候

舉例來說,一個下載任務如果不只是回報「完成了」這一個結果,而是要持續回報「目前下載到百分之多少」這種連續進度,Dispatchers、SupervisorJob、CoroutineExceptionHandler 這些工具都幫不上忙,它們服務的都是一次性任務的執行與治理,沒有一個是為了持續產生多個值而存在。

Kotlin 協程之上,建立了一種可以依序發送多個值的非同步資料流型別,這就是 Flow。從今天開始,全系列一律使用這個英文詞彙,不另外翻譯成中文。

suspend function 天生只能回傳一個值

要理解 Flow 為什麼需要存在,得先回到 Day 02:第一個 suspend function,暫停到底暫停了什麼 定案的心智模型。一個 suspend function 無論內部暫停多久、經歷多少次暫停與恢復,最終呼叫端拿到的,都是一次性的單一回傳值。

這個設計本身沒有任何問題,因為它服務的正是「執行一次拿到一個結果」這種任務形式,下載一張圖片、拿到一份聚合結果,都完美落在這個形狀裡。

問題出現在情境改變的時候。

回到多來源圖片下載與聚合工具,如果想讓一個下載函式在下載過程中持續回報目前完成的百分比,而不是只等到下載完成才回傳一次結果,用 suspend function 的設計,沒有一個自然的方式可以做到這件事。

suspend function 的回傳型別只能對應一次性的結果,它沒有「回傳到一半、再回傳一次」這種語意。

勉強要做,開發者得自己額外設計某種回呼機制,在下載進度更新的每個時間點,主動呼叫外部傳入的一個函式,把目前的百分比丟出去。這聽起來很熟悉。Day 01:為什麼你需要協程,從一個會卡住主執行緒的下載函式說起 提過,回呼式寫法天生難以組合、難以取消,程式碼的形狀會被異步處理本身扭曲。繞了一大圈,為了讓一個任務能持續回報多個值,又走回了協程原本想解決的那個問題。

這正是 Flow 存在的動機。它讓「依序發送多個值」這件事,成為一個型別本身就內建支援的能力,不需要開發者自己額外拼裝一套回呼機制去外部回報進度。

Flow 的三個核心特性

Flow 具備三個核心特性,建立起今天最重要的心智模型。

第一個特性是依序發送。一個 Flow 可以在其生命週期內,依序發送零個、一個或多個值,每個值會依照發送順序被收集端依序接收處理。這正是 Flow 與 suspend function 最直接的差異:一個回傳的是單一值,一個回傳的是一串隨時間展開的值序列。前面提到的下載進度,就是一個很自然的例子,百分之十、百分之三十、百分之七十,這些數值依序被發送出去,而不是打包成一份結果一次交出。

第二個特性是冷流。一個 Flow 在被定義出來的當下並不會立刻開始執行內部邏輯,必須等到有人真正開始收集它,才會啟動整個發送流程,而且每次收集都會重新觸發一次完整的執行。這有點像水龍頭沒打開就不會有水流出來,打開了才開始流。這裡只建立現象層級的認識,冷流與熱流的完整對照,留到 《Day 21:StateFlow 與 SharedFlow,冷流不夠用的時候》 才會展開。

第三個特性是可取消。收集一個 Flow 的動作,是在某個協程裡進行的,這個協程依然遵守 Day 10:協程取消不是強制中斷,是一場合作 定案的協程取消機制。如果收集所在的協程被取消,這個 Flow 的發送流程也會跟著停止。這個呼應關係只需要點出來,取消機制的完整規則不重新展開。

Flow 依然活在結構化並發框架裡

這三個特性,容易讓人誤以為 Flow 是一套全新、跟前面十八天無關的獨立技術,這裡要明確把這個誤解排除掉。

Flow 本身建立在協程之上。收集一個 Flow 的動作,本質上是在某個 Coroutine Scope 裡的某個協程中進行的,這個協程依然遵守 Day 07:Structured Concurrency,為什麼協程不能亂長亂放 定案的父子收斂規則。並沒有因為換成了 Flow 這個新型別,就脫離了系列一路建立起來的生命週期管理框架,Flow 是這套框架之上長出來的新能力,不是另立山頭的獨立技巧。

這個定位值得特別強調,因為它會影響接下來學習 Flow 的方式。往後遇到任何跟 Flow 生命週期、取消、例外相關的疑問,都可以優先回頭用前面幾個階段建立的心智模型去理解,而不是把 Flow 當成一套需要重新學習的知識體系。

收集所在的協程被取消了,Flow 自然停止;父範圍還沒收斂前,子協程裡的 Flow 收集動作也還沒真正結束,這些判斷邏輯,跟階段二、階段三建立的規則完全一致。

動手建立並收集一個最簡單的 Flow

概念談了不少,來看一眼最基礎的語法樣貌。建立一個 Flow,可以用 flow 建構器搭配 emit 依序發送值:

fun downloadProgress(): Flow<Int> = flow {
    emit(10)
    delay(300)
    emit(30)
    delay(300)
    emit(70)
    delay(300)
    emit(100)
}

這段程式碼把多來源圖片下載與聚合工具中「持續回報下載進度百分比」這個需求,對應成一個依序發送進度數值的 Flow。要注意的是,flow 區塊裡的內容在這裡並不會執行,前面提過的冷流特性,正是這個現象的來源。

要真正啟動這串發送流程,需要收集它,最基礎的方式是使用 collect:

suspend fun showDownloadProgress() {
    downloadProgress().collect { percent ->
        println("目前下載進度:$percent%")
    }
}

每呼叫一次 collect,flow 區塊內的邏輯就會重新完整執行一次,依序把 10、30、70、100 這幾個值送進 collect 的 lambda 裡處理。範例只停在建立與收集這兩個基本動作,不涉及任何轉換或篩選這些數值的操作符用法。

拿到這串值之後,能不能像操作集合一樣處理它

今天知道了 Flow 可以依序發送多個值,也可以被收集,而且它依然活在結構化並發的框架裡,收集所在的協程一樣遵守父子收斂與取消機制的規則。

但如果拿到的這一串進度數值,需要過濾掉不需要的中間值,或者轉換成另一種格式才呈現給使用者,難道每個值都得自己在 collect 的 lambda 裡手動判斷跟改寫嗎?如果收集邏輯裡混雜了太多這類判斷,程式碼很快又會回到難以閱讀的老路上。

下一篇會示範這個問題的答案,詳見 《Day 20:Flow 的操作符,map、filter、collect 這些常用招式》,Flow 的操作符如何把這些值組合成一套資料處理管線。


上一篇
Day 18:小結:協程的執行環境與例外治理工具箱總覽
下一篇
Day 20:Flow 的操作符,map、filter、collect 這些常用招式
系列文
異步程式設計修煉:Kotlin Coroutines 完全指南 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言